package com.mrx.xfriendship.filter;

import com.mrx.commons.config.Constant;
import com.mrx.commons.exception.BusinessException;
import com.mrx.commons.util.JWTUtil;
import com.mrx.commons.util.asserts.Asserts;
import com.mrx.xfriendship.service.IRedisService;
import com.mrx.xfriendship.service.ISecurityUserService;
import com.mrx.xfriendship.service.IUserService;
import com.mrx.xfriendship.util.SecurityUtil;
import org.apache.dubbo.config.annotation.DubboReference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;

import javax.annotation.Resource;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Optional;

/**
 * @author : 肖康宁
 * @since : 2022-12-08 14:55
 **/
@Component
public class JWTFilter extends OncePerRequestFilter implements HandshakeInterceptor {

    private static final Logger logger = LoggerFactory.getLogger(JWTFilter.class);

    @Resource
    private ISecurityUserService securityUserService;

    @DubboReference
    private IRedisService redisService;

    @DubboReference
    private IUserService userService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException {
        String token = request.getHeader(Constant.HEADER_TOKEN);
        if (!request.getRequestURI().equals("/user/heartBeat"))
            logger.debug("doFilter: {}, {}, token: {}", request.getRequestURI(), request.getMethod(), token);
        try {
            tokenHandler(token);
            filterChain.doFilter(request, response);
        } catch (BusinessException e) {
            // 保证 token 过期也能访问 公开资源
            logger.warn("JWTFilter 常见异常: {}", e.getLocalizedMessage());
            filterChain.doFilter(request, response);
        } catch (Exception e) {
            logger.error("JWTFilter 未知异常: {}", e.getLocalizedMessage());
            // 过滤器全局异常拦截
            request.setAttribute(Constant.ERROR_ATTRIBUTE, e);
            request.getRequestDispatcher(Constant.ERROR_ROUTE).forward(request, response);
        }
    }

    @Override
    public boolean beforeHandshake(
            @NonNull ServerHttpRequest request,
            @NonNull ServerHttpResponse response,
            @NonNull WebSocketHandler wsHandler,
            @NonNull Map<String, Object> attributes
    ) {
        List<String> tokenList = request.getHeaders().get(Constant.WEBSOCKET_HEADER_TOKEN);
        if (CollectionUtils.isEmpty(tokenList)) {
            response.setStatusCode(HttpStatus.FORBIDDEN);
            logger.warn("拦截无 token 的请求: {}, {}, tokenList: {}", request.getURI(), request.getMethod(), tokenList);
            return false;
        }
        String token = tokenList.get(0);
        logger.debug("beforeHandshake: {}, {}, token: {}", request.getURI(), request.getMethod(), token);
        // 将 UserDTO 放入 attributes
        attributes.put(Constant.WEBSOCKET_ATTRIBUTE_USER, userService.selectUserByName(tokenHandler(token)));
        return true;
    }

    @Override
    public void afterHandshake(
            @NonNull ServerHttpRequest request,
            @NonNull ServerHttpResponse response,
            @NonNull WebSocketHandler wsHandler,
            Exception exception
    ) {
        HttpHeaders headers = response.getHeaders();
        // 如果 requestHeader 包含 WEBSOCKET_HEADER_TOKEN, 那响应里也得包含这个, 不然会导致 客户端无法连接
        Optional.ofNullable(request.getHeaders().get(Constant.WEBSOCKET_HEADER_TOKEN))
                .ifPresent(it -> headers.set(Constant.WEBSOCKET_HEADER_TOKEN, it.get(0)));
        logger.debug("握手完毕: {}", headers);
    }

    /**
     * 校验 token, 如果 token 存在且有效则 设置对应的 Authentication<br/>
     * 如果 校验失败 或 出现其它异常 则会抛出异常
     *
     * @param token token
     */
    private String tokenHandler(@Nullable String token) {
        // 如果没有设置 token header, 那就 跳过本次过滤器
        if (StringUtils.hasText(token)) {
            // 从 token 中解析出 用户名
            String userName = JWTUtil.parseToken(token);
            // 判断 token 是否存在与 redis 中, 并且与当前 header 的 token 相等, 若不相等, 则表明 token 已过期
            Asserts.TOKEN_EXPIRED.checkTrue(redisService.getUserToken(userName).equals(token), Constant.TOKEN_INVALID);
            // 如果 token 校验通过, 那就将用户信息设置到 SecurityContext
            SecurityUtil.setAuthentication(securityUserService.selectUserByName(userName).toAuthentication());
            return userName;
        } else {
            logger.debug("JWTFilter 未找到 token");
        }
        return "null";
    }

}
